//@version=6
indicator("Auto Trend [theUltimator5]", overlay=true, max_bars_back=5000)

// Input for lookback period
use_auto_lookback = input.bool(true, "Use Auto Lookback", tooltip="Automatically find the best lookback period where all bars close within the lines")
pattern_length = input.string("Long", "Pattern Length", options=["Short", "Long"], tooltip="Short: 20-66 by 1 (faster), Long: 30-350 by 5")
manual_lookback = input.int(100, "Manual Lookback Period", minval=30)
min_lookback_long = input.int(50, "Min Lookback (Long Pattern)", minval=30, tooltip="Minimum number of bars to look back for Long pattern")
max_lookback_cap = input.int(500, "Max Lookback Cap (Long Pattern)", minval=30, tooltip="Maximum number of bars to look back for Long pattern")
show_debug_label = input.bool(false, "Show Trendline Label", tooltip="Display label with lookback period and count of times chart closed outside of the top and bottom lines")
show_pivot_markers = input.bool(false, "Show Pivot Point Markers", tooltip="Display arrow markers at the high/low points used for trendlines")
price_source = input.string("Close", "Price Source", options=["Close", "High/Low"], tooltip="Use close prices or high/low wicks for calculations")

// Color inputs for the three lines
high_line_color = input.color(color.gray, "High Line Color")
low_line_color = input.color(color.gray, "Low Line Color")
mid_line_color = input.color(color.aqua, "Middle Line Color")

// Determine if using wicks based on selection
use_wicks = price_source == "High/Low"

// Input for starting time (allows user to click on chart to select a point)
start_time = input.time(timestamp("2024-01-01 00:00"), title="Select Starting Point", confirm=true)

// Store the bar index corresponding to the selected time
var int stored_bar_index = na

// Find the bar index that corresponds to the selected time
var int start_bar = 0
var bool time_found = false

// Try to find the exact time match
for i = 0 to bar_index
    if time[i] <= start_time
        start_bar := i
        time_found := true
        if na(stored_bar_index) or start_time != start_time[1]
            stored_bar_index := i
        break

if not time_found and not na(stored_bar_index)
    start_bar := math.min(stored_bar_index, bar_index)

var int cached_optimal_lookback = na
var int last_calc_bar = na
var int cache_start_time = na
var int cache_start_bar = na

bool start_bar_changed = cache_start_bar != start_bar
bool should_recalculate = na(cached_optimal_lookback) or 
                          cache_start_time != start_time or 
                          (barstate.isconfirmed and (start_bar_changed or (bar_index - last_calc_bar) >= 10))

// Function to count violations for a given lookback
countViolations(test_lookback, s_bar, max_violations, use_wicks_param) =>
    // Calculate thirds.  This is used to find high and low points
    // close to both the start and finish of the pattern.
    // It intentionally excludes points in the middle of the pattern
    // in order to limit shape warping due to outliers.
    first_start = test_lookback + s_bar
    first_end = int(2 * test_lookback / 3) + s_bar
    last_start = int(test_lookback / 3) + s_bar
    last_end = s_bar
    
    // Safety checks to ensure we don't exceed bar_index
    safe_first_start = math.min(first_start, bar_index)
    safe_first_end = math.min(first_end, bar_index)
    safe_last_start = math.min(last_start, bar_index)
    safe_last_end = math.min(last_end, bar_index)
    
    // Find highs and lows in first third
    f_high = use_wicks_param ? high[safe_first_end] : close[safe_first_end]
    f_high_bar = safe_first_end
    for i = safe_first_end to safe_first_start
        current_high = use_wicks_param ? high[i] : close[i]
        if current_high > f_high
            f_high := current_high
            f_high_bar := i
    
    // Find highs in last third
    l_high = use_wicks_param ? high[safe_last_end] : close[safe_last_end]
    l_high_bar = safe_last_end
    for i = safe_last_end to safe_last_start
        current_high = use_wicks_param ? high[i] : close[i]
        if current_high > l_high
            l_high := current_high
            l_high_bar := i
    
    // Find lows in first third
    f_low = use_wicks_param ? low[safe_first_end] : close[safe_first_end]
    f_low_bar = safe_first_end
    for i = safe_first_end to safe_first_start
        current_low = use_wicks_param ? low[i] : close[i]
        if current_low < f_low
            f_low := current_low
            f_low_bar := i
    
    // Find lows in last third
    l_low = use_wicks_param ? low[safe_last_end] : close[safe_last_end]
    l_low_bar = safe_last_end
    for i = safe_last_end to safe_last_start
        current_low = use_wicks_param ? low[i] : close[i]
        if current_low < l_low
            l_low := current_low
            l_low_bar := i
    
    // Calculate slopes
    h_slope = (l_high - f_high) / (bar_index[l_high_bar] - bar_index[f_high_bar])
    l_slope = (l_low - f_low) / (bar_index[l_low_bar] - bar_index[f_low_bar])
    
    // Count violations with EARLY EXIT
    violation_count = 0
    safe_test_start = math.min(test_lookback + s_bar, bar_index)
    for j = s_bar to safe_test_start
        high_val = f_high + h_slope * (bar_index[j] - bar_index[f_high_bar])
        low_val = f_low + l_slope * (bar_index[j] - bar_index[f_low_bar])
        
        test_high = use_wicks_param ? high[j] : close[j]
        test_low = use_wicks_param ? low[j] : close[j]
        
        if test_high > high_val or test_low < low_val
            violation_count += 1
            // EARLY EXIT: If we already exceeded max violations, stop counting
            if violation_count > max_violations
                break
    
    violation_count

// Auto-find optimal lookback
int optimal_lookback = na
if use_auto_lookback and should_recalculate
    // Determine search parameters based on pattern length
    int start_lb = pattern_length == "Short" ? 20 : min_lookback_long
    int end_lb = pattern_length == "Short" ? 66 : max_lookback_cap
    int increment = pattern_length == "Short" ? 1 : 5
    
    // Phase 1: Search with specified parameters
    found_initial = false
    int temp_optimal = na
    int min_violations = 999999
    int best_lookback_with_min_violations = na
    
    for lb = start_lb to end_lb by increment
        // Skip this lookback if it would exceed available historical data
        if lb + start_bar > bar_index
            continue
        if lb + start_bar <= 5000
            // Count violations for this lookback
            violations = countViolations(lb, start_bar, min_violations, use_wicks)
            
            // Track the best option (lookback with minimum violations)
            // If violations are less, this is definitely better
            // If violations are equal, keep the earlier (smaller) lookback
            if violations < min_violations
                min_violations := violations
                best_lookback_with_min_violations := lb
            
            // If we find a perfect match (0 violations), mark it
            if violations == 0 and not found_initial
                temp_optimal := lb
                found_initial := true
                // Don't break - keep searching for longer perfect matches
    
    // Phase 2: If found perfect match, we already tracked the longest one in Phase 1
    // But let's continue from the last perfect match to find where it breaks
    if found_initial
        // temp_optimal has the first perfect match
        // best_lookback_with_min_violations has the last perfect match (since min_violations = 0)
        // Now search beyond end_lb if needed to find the true limit
        int last_good = best_lookback_with_min_violations
        for test_lb = best_lookback_with_min_violations + increment to end_lb by increment
            if test_lb + start_bar > bar_index
                break
            if test_lb + start_bar <= 5000
                if countViolations(test_lb, start_bar, 0, use_wicks) == 0
                    last_good := test_lb
                else
                    break
            else
                break
        optimal_lookback := last_good
    else
        // No perfect match found, use the one with least violations
        optimal_lookback := best_lookback_with_min_violations
    
    // Cache the result
    cached_optimal_lookback := optimal_lookback
    last_calc_bar := bar_index
    cache_start_time := start_time
    cache_start_bar := start_bar
else if use_auto_lookback
    // Use cached value
    optimal_lookback := cached_optimal_lookback

lookback = use_auto_lookback and not na(optimal_lookback) ? optimal_lookback : manual_lookback

// Ensure lookback + start_bar doesn't exceed Pine Script's limit
// Also ensure we don't exceed bar_index (available historical data)
// This must account for the fact that bar_index can change between calculations
max_available = bar_index - start_bar
effective_lookback = lookback

// Apply all constraints
if lookback + start_bar > 5000
    effective_lookback := 5000 - start_bar - 1
if effective_lookback > max_available
    effective_lookback := max_available
    
// Additional safety: ensure effective_lookback + start_bar never exceeds bar_index
if effective_lookback + start_bar > bar_index
    effective_lookback := bar_index - start_bar

// Final safety: ensure effective_lookback is positive
if effective_lookback < 1
    effective_lookback := 1

// Calculate the size of each third
third = effective_lookback / 3

// Adjust all bar references to account for the starting bar offset
// Get the first third (oldest data) - from close[effective_lookback+start_bar] to close[2*effective_lookback/3+start_bar]
first_third_start = effective_lookback + start_bar
first_third_end = int(2 * effective_lookback / 3) + start_bar

// Get the last third (most recent data) - from close[effective_lookback/3+start_bar] to close[start_bar]
last_third_start = int(effective_lookback / 3) + start_bar
last_third_end = start_bar

// Find highest close in first third
var float first_third_high = na
var int first_third_high_bar = na
// Safety check: ensure initial assignment doesn't exceed available bars
safe_first_end = math.min(first_third_end, bar_index)
first_third_high := use_wicks ? high[safe_first_end] : close[safe_first_end]
first_third_high_bar := safe_first_end

// Safety check: ensure we don't exceed available bars for both start and end
safe_first_start = math.min(first_third_start, bar_index)
for i = safe_first_end to safe_first_start
    current_high = use_wicks ? high[i] : close[i]
    if current_high > first_third_high
        first_third_high := current_high
        first_third_high_bar := i

// Find highest close in last third
var float last_third_high = na
var int last_third_high_bar = na
safe_last_end = math.min(last_third_end, bar_index)
last_third_high := use_wicks ? high[safe_last_end] : close[safe_last_end]
last_third_high_bar := safe_last_end

safe_last_start = math.min(last_third_start, bar_index)
for i = safe_last_end to safe_last_start
    current_high = use_wicks ? high[i] : close[i]
    if current_high > last_third_high
        last_third_high := current_high
        last_third_high_bar := i

// Find lowest close in first third
var float first_third_low = na
var int first_third_low_bar = na
safe_first_end_low = math.min(first_third_end, bar_index)
first_third_low := use_wicks ? low[safe_first_end_low] : close[safe_first_end_low]
first_third_low_bar := safe_first_end_low

safe_first_start_low = math.min(first_third_start, bar_index)
for i = safe_first_end_low to safe_first_start_low
    current_low = use_wicks ? low[i] : close[i]
    if current_low < first_third_low
        first_third_low := current_low
        first_third_low_bar := i

// Find lowest close in last third
var float last_third_low = na
var int last_third_low_bar = na
safe_last_end_low = math.min(last_third_end, bar_index)
last_third_low := use_wicks ? low[safe_last_end_low] : close[safe_last_end_low]
last_third_low_bar := safe_last_end_low

safe_last_start_low = math.min(last_third_start, bar_index)
for i = safe_last_end_low to safe_last_start_low
    current_low = use_wicks ? low[i] : close[i]
    if current_low < last_third_low
        last_third_low := current_low
        last_third_low_bar := i

// Calculate slopes for each line so we can extend them
// High line slope
high_slope = (last_third_high - first_third_high) / (bar_index[last_third_high_bar] - bar_index[first_third_high_bar])

// Calculate high line values at the endpoints (bar_index[effective_lookback+start_bar] and bar_index[start_bar])
high_at_lookback = first_third_high + high_slope * (bar_index[effective_lookback + start_bar] - bar_index[first_third_high_bar])
high_at_close0 = last_third_high + high_slope * (bar_index[start_bar] - bar_index[last_third_high_bar])

// Low line slope
low_slope = (last_third_low - first_third_low) / (bar_index[last_third_low_bar] - bar_index[first_third_low_bar])

// Calculate low line values at the endpoints
low_at_lookback = first_third_low + low_slope * (bar_index[effective_lookback + start_bar] - bar_index[first_third_low_bar])
low_at_close0 = last_third_low + low_slope * (bar_index[start_bar] - bar_index[last_third_low_bar])

// Calculate mid line values at the endpoints
mid_at_lookback = (high_at_lookback + low_at_lookback) / 2
mid_at_close0 = (high_at_close0 + low_at_close0) / 2

// Show the lookback value being used and verify bars are within lines (only if toggle is enabled)
if barstate.islast and show_debug_label
    var label debug_label = na
    label.delete(debug_label)
    lookback_text = use_auto_lookback ? "Auto: " + str.tostring(lookback) : "Manual: " + str.tostring(lookback)
    
    // Show if auto search failed
    if use_auto_lookback and na(optimal_lookback)
        lookback_text := lookback_text + " (NO VALID PATTERN FOUND - using manual)"
    
    // Check if any bars are actually outside
    violations_count = 0
    safe_end_check = math.min(effective_lookback + start_bar, bar_index)
    for j = start_bar to safe_end_check
        high_val = first_third_high + high_slope * (bar_index[j] - bar_index[first_third_high_bar])
        low_val = first_third_low + low_slope * (bar_index[j] - bar_index[first_third_low_bar])
        test_high = use_wicks ? high[j] : close[j]
        test_low = use_wicks ? low[j] : close[j]
        if test_high > high_val or test_low < low_val
            violations_count += 1
    
    lookback_text := lookback_text + " | External closes: " + str.tostring(violations_count) + " | Bar Index: " + str.tostring(start_bar)
    debug_label := label.new(bar_index[start_bar], high_at_close0, lookback_text, style=label.style_label_down, color=color.yellow, textcolor=color.black)

// Draw arrow markers at pivot points (only if toggle is enabled)
var label first_high_marker = na
var label last_high_marker = na
var label first_low_marker = na
var label last_low_marker = na

if barstate.islast and show_pivot_markers
    // Delete old markers
    label.delete(first_high_marker)
    label.delete(last_high_marker)
    label.delete(first_low_marker)
    label.delete(last_low_marker)
    
    // Create new markers at the pivot points
    // Down triangles for high points (at the high of the bar)
    first_high_marker := label.new(bar_index[first_third_high_bar], high[first_third_high_bar], 
                                    "", style=label.style_triangledown, 
                                    color=high_line_color, size=size.small)
    
    last_high_marker := label.new(bar_index[last_third_high_bar], high[last_third_high_bar], 
                                   "", style=label.style_triangledown, 
                                   color=high_line_color, size=size.small)
    
    // Up triangles for low points (at the low of the bar)
    first_low_marker := label.new(bar_index[first_third_low_bar], low[first_third_low_bar], 
                                   "", style=label.style_triangleup, 
                                   color=low_line_color, size=size.small)
    
    last_low_marker := label.new(bar_index[last_third_low_bar], low[last_third_low_bar], 
                                  "", style=label.style_triangleup, 
                                  color=low_line_color, size=size.small)

// Draw the three lines extending from the lookback position to the starting bar
var line high_line = na
if barstate.islast
    line.delete(high_line)
    high_line := line.new(bar_index[effective_lookback + start_bar], high_at_lookback,
                          bar_index[start_bar], high_at_close0, 
                          color=high_line_color, width=2)

var line low_line = na
if barstate.islast
    line.delete(low_line)
    low_line := line.new(bar_index[effective_lookback + start_bar], low_at_lookback,
                         bar_index[start_bar], low_at_close0, 
                         color=low_line_color, width=2)

var line mid_line = na
if barstate.islast
    line.delete(mid_line)
    mid_line := line.new(bar_index[effective_lookback + start_bar], mid_at_lookback,
                         bar_index[start_bar], mid_at_close0, 
                         color=mid_line_color, width=1)
